Skip to content

dash: wire the embedded arm so --run can actually take it (#738) - #891

Merged
frstrtr merged 3 commits into
masterfrom
dash/wire-embedded-arm-in-run
Jul 28, 2026
Merged

dash: wire the embedded arm so --run can actually take it (#738)#891
frstrtr merged 3 commits into
masterfrom
dash/wire-embedded-arm-in-run

Conversation

@frstrtr

@frstrtr frstrtr commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Draft. Refs #738.

The exact reason the embedded arm was never taken

Taking the daemonless embedded template arm requires two independent conditions, and before this PR no single flag satisfied both:

  1. The work-source arm gateDASHWorkSource only consults the embedded bundle when is_testnet_ || embedded_mainnet_ (src/impl/dash/stratum/work_source.cpp:327, guard at :205). On mainnet that means --embedded-mainnet.
  2. A live coin-state feedNodeCoinState::populated() only flips once CoinStateMaintainer publishes a tip, and the maintainer + header chain + 5 ingest legs are constructed only inside main_dash.cpp's if (coin_p2p) block (src/c2pool/main_dash.cpp:2164). coin_p2p itself was built only under if (!coin_p2p_targets.empty() || coin_p2p_discover).

--embedded-mainnet armed (1) only — the bundle was never fed, populated() stayed false forever, and every template came off the dashd fallback. --coin-p2p-connect / --coin-p2p-discover armed (2) only. On top of that --embedded-mainnet was absent from --help entirely, so even the undocumented two-flag combination was undiscoverable.

Net effect: 100% of released --run templates came off the dashd fallback, and the entire merged, soak-proven daemonless stack (#826: SML/quorum CCbTx, DKG-window serving, BLS-verified commitments, superblock/credit-pool/payee guards) was unreachable scaffolding.

What was wired

resolve_embedded_arm() (new src/impl/dash/coin/arm_resolution.hpp) makes the decision once, as one pure, dependency-free function, with a strictly one-way implication:

embedded opt-in  =>  coin-state feed     (NEW — this is the wiring)
coin-state feed  =>  embedded opt-in     (NEVER — explicitly not)

--embedded-mainnet now arms both halves end to end (seed-based discovery when no peer is pinned; an operator-named transport is never overridden). A transport flag still cannot move the arm. main_dash.cpp is the sole production caller; the four testnet || embedded_mainnet sites and the coin-P2P construction predicate now read from the single resolution. --embedded-mainnet is documented in --help, and a startup line names the resolved arm plus the reason, so the arm is field-checkable instead of inferred.

Reward safety — how default-arm behaviour was proven unchanged

This node class runs a live production mining hotel, and there is a documented incident where --coin-p2p-connect activated an unguarded embedded arm on a live node.

Proof by construction. For a default run (testnet=false, embedded_mainnet=false, no coin-P2P flags) every changed predicate evaluates to the identical value:

site before after default run
coin-P2P construction !coin_p2p_targets.empty() || coin_p2p_discover run_arm.coin_feed_armed false -> false
arm gate (x4) testnet || embedded_mainnet run_arm.embedded_arm_enabled same expression, false
discovery coin_p2p_discover coin_p2p_discover_eff false -> false

work_source->set_embedded_mainnet(embedded_mainnet) still reads the raw flag, not the resolution — defence in depth: even a wrong resolver cannot lift the work-source gate.

Proof by A/B binary diff. Pre-change and post-change c2pool-dash binaries were built from the same tree and run on both non-embedded invocations. Coin-network subsystem activity is identical:

invocation PRE EMB-DASH/COIN-P2P lines POST
default --run 0 0
--coin-p2p-connect alone 6 6

Normalized control-flow diff shows only the help text and the two new diagnostic lines; the residual is live-sharechain P2P timing noise of the same class a PRE-vs-PRE self-diff control also produces.

Observed at runtime:

--run                     -> [run] template arm=dashd-fallback — no --embedded-mainnet (default reward-safe posture)
                             (embedded_arm_enabled=no coin_state_feed=off)
--coin-p2p-connect alone  -> [run] template arm=dashd-fallback — coin-P2P feed requested but --embedded-mainnet
                             absent (transport only; the arm never follows the transport)
                             (embedded_arm_enabled=no coin_state_feed=armed)
--embedded-mainnet        -> [run] template arm=embedded — embedded opt-in + coin-state feed both present
                             (embedded_arm_enabled=yes coin_state_feed=armed discovery=implied-by-opt-in)

Reachability proof (not just a passing test)

Per the #873 / #878 / #881 lesson — passing tests wired into unreachable code — the wiring was proven reachable in the real binary against live mainnet, not only in a test mirror:

[EMB-DASH] HeaderChain::init() ... genesis=00000ffd590b1485
[EMB-DASH] handshake complete -> initial sync: getheaders + mempool + mnlistdiff(cold) + getaddr
[EMB-DASH] tip advanced h=2402000 ... -> new_tip fired (maintainer arm)
[SML] applied diff: SML +3213 -0 => 3213 MNs; quorums +88 -0 => 88 active; have_sml=yes

That new_tip fired (maintainer arm) line is the exact leg that flips populated() — previously dead in every released binary.

Caller-side lock trace: the wiring adds no call into a self-locking method. resolve_embedded_arm() is pure (no I/O, no allocation, no locks) and is called once in run_node before ioc.run(), i.e. single-threaded startup. NodeCoinState holds no mutex at all — it is io_context-thread confined, with maintainer writes and get_work() reads on the same thread — so the "exclusive lock held across a self-locking callee" defect class does not arise. The only changed values are startup-time predicates.

Tests

Folded into the existing allowlisted test_dash_stratum_work_source target (.github/workflows/build.yml:147 and :335) — no new add_executable, per #868.

New DashRunArmResolution suite, 8 tests. Each goes beyond restating the resolver: served_arm_for() composes the resolution exactly as run_node does and asks the real DASHWorkSource::get_work() which arm it serves.

  • DefaultRunResolvesDashdFallback — reward-safety pin 1
  • CoinP2pConnectAloneStillResolvesDashdFallback — reward-safety pin 2 (the hotel incident)
  • CoinP2pDiscoverAloneStillResolvesDashdFallback
  • EmbeddedMainnetOptInArmsBothHalvesEndToEnd — the wiring
  • EmbeddedMainnetWithPinnedPeerDoesNotImplyDiscovery
  • TestnetWithoutFeedIsUnchangedFallback / TestnetWithCoinP2pIsUnchangedEmbedded — no regression
  • NoMainnetCombinationWithoutOptInEverEnablesTheArm — exhaustive sweep of the un-opted-in mainnet argv quadrant

Result: 52/52 pass in the full target.

Negative controls (both confirm the pins actually bite):

Scope / what this does NOT do

This closes item 1 of #738's four-item EMBEDDED-READY list (wire the feed into the work source). Items 2-4 remain open: BIP35 connect-time mempool pull, the fee_known pricing ladder, and the live differential re-run as acceptance gate. Every per-template viability gate (SML fresh at tip, non-superblock, credit-pool seed height, bestCL, MN-payee cursor, DKG plan) still fails closed to the dashd fallback — arming the arm is not the same as serving from it. Observed in the live run: E2c MN-set seed UNAVAILABLE ... templates keep routing to the fallback arm.

Related, NOT fixed here (owned by a sibling agent)

C2POOL_DASH_BLS=ON is set by no CI or release workflow (grep -rn C2POOL_DASH_BLS .github/workflows/ -> zero hits; the option defaults OFF at CMakeLists.txt:122). Shipped binaries are therefore BLS-dark and Phase-L can never serve a real commitment in the field.

Trap for the record: the flag ON with the lib ABSENT is a message(WARNING ...), not FATAL_ERROR (CMakeLists.txt:154) — it sets C2POOL_DASH_BLS_AVAILABLE=OFF, compiles the fail-closed null-serve stub, and the build still succeeds. A CI job that flips the flag without also providing dashbls would look green while shipping the stub.

frstrtr and others added 3 commits July 27, 2026 20:07
The daemonless embedded template stack (SML/quorum CCbTx, DKG-window
serving, BLS-verified commitments, superblock/credit-pool/payee guards)
is merged and soak-proven, but no released --run invocation could reach
it. Taking the embedded arm needs TWO independent conditions:

  (1) the work-source arm gate -- DASHWorkSource only consults the
      embedded bundle when `is_testnet_ || embedded_mainnet_`
      (work_source.cpp get_work / resource_template_now);
  (2) a live coin-state FEED -- NodeCoinState::populated() only flips
      when CoinStateMaintainer publishes a tip, and the maintainer +
      header chain + 6 ingest legs are constructed ONLY inside
      main_dash's `if (coin_p2p)` block.

No single flag satisfied both. --embedded-mainnet armed (1) only, so
the bundle was never fed and populated() stayed false forever;
--coin-p2p-connect/--coin-p2p-discover armed (2) only. On top of that
--embedded-mainnet was absent from --help entirely, so the undocumented
combination was undiscoverable. Net effect: 100% of released --run
templates came off the dashd fallback and the whole daemonless stack
was unreachable scaffolding.

resolve_embedded_arm() (arm_resolution.hpp) makes the decision once, as
one pure function, with a strictly ONE-WAY implication:

    embedded opt-in  =>  coin-state feed     (new -- this is the wiring)
    coin-state feed  =>  embedded opt-in     (never -- explicitly not)

So --embedded-mainnet now arms both halves end to end (seed-based
discovery when no peer is pinned), while a transport flag still cannot
move the arm.

REWARD SAFETY (this node class runs a live production mining hotel, and
there is a documented incident where --coin-p2p-connect activated an
unguarded embedded arm on a live node):

  * a default --run resolves arm=dashd-fallback, unchanged;
  * --coin-p2p-connect alone resolves arm=dashd-fallback, unchanged --
    it brings up the transport and nothing else;
  * both are pinned by tests, including an exhaustive sweep of the
    un-opted-in mainnet argv quadrant;
  * verified empirically: normalized run-log diff of the pre-change vs
    post-change binary for both invocations differs ONLY by the two
    added diagnostic lines.

Also: --embedded-mainnet documented in --help, and a startup line that
names the resolved arm plus the reason, so the arm is field-checkable
instead of inferred.

Tests fold into the existing allowlisted test_dash_stratum_work_source
target (no new add_executable -> no NOT_BUILT sentinel); the allowlist
drift-guard passes.
The E1 block comment still said coin_p2p is null without
--coin-p2p-connect; --embedded-mainnet now implies a feed. Restate the
no-op guarantee over the resolved feed condition and spell out that
arming the feed alone still cannot move the arm. Comment-only.
The DASH coin peer manager exposed set_http_peer_seeds (coin_peer_manager.hpp:303) as the third bootstrap fallback tier (after DNS + fixed seeds) but main_dash.cpp never called it, leaving that tier dark. LTC wires it at main_ltc.cpp:1875. Add the equivalent call in the coin_p2p_discover arm so the daemonless embedded path has the same three-tier peer bootstrap.
@frstrtr
frstrtr force-pushed the dash/wire-embedded-arm-in-run branch from 0ca0de0 to a41e9c2 Compare July 27, 2026 20:09
@frstrtr
frstrtr marked this pull request as ready for review July 27, 2026 20:58
@frstrtr

frstrtr commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

The Windows red is NOT clearing, and it is #891-specific

Re-ran and re-measured. Correcting the earlier "secp256k1 git-clone flake" read — it survived a re-run, which a clone flake would not.

failing job   Windows x86_64
failing step  Build secp256k1

Isolated to this PR. The same Windows check passes right now on:

#930  Windows: pass
#931  Windows: pass
#880  Windows: pass

Three other open PRs, same runners, same step, green. So this is not a runner or dependency-fetch problem — the asymmetry points at #891.

Why I am not calling it, and what I need from you

I have mis-diagnosed CI failures in both directions this week and would rather hand you evidence than a guess:

So: Build secp256k1 failing only here, persisting across a re-run, is the shape of a real defect — but I have not read the actual compiler/linker output and will not assert a cause without it.

Please pull the raw step log. Two things worth checking first:

  1. Did this PR change anything that affects the dependency build — CMake, Conan profile, a link requirement, a toolchain flag? The diff is +402/-17 across 3 files, so it should be quick to rule in or out.
  2. Is /bigobj relevant again? build(windows): add /bigobj for MSVC so c2pool-dash links (fixes #909) #917 (7ff1d6e7) added add_compile_options(/bigobj) inside if(MSVC) because main_dash.cpp crossed the 65,279-section COFF limit. This PR adds 402 lines to that same lane. If the rebase picked up build(windows): add /bigobj for MSVC so c2pool-dash links (fixes #909) #917 the fix is already present — confirm it did rather than assuming, since the failing step is named secp256k1 and not the coin binary.

Why this matters more than a normal red

#891 is the contabo blocker, and now with live evidence behind it. A 30 h daemonless soak measured 0 coin-network peers against 4 sharechain peers — same process, same host. The coin dialer never runs, because --embedded-mainnet sets neither coin_p2p_targets nor coin_p2p_discover and main_dash.cpp:1244 gates on exactly that. Your arm_resolution.hpp:108 closes it.

Contabo has since been rebuilt to 767afe76 and restarted, so the moment this lands it can be re-run and will actually reach the E2c wall — which is its assigned purpose.

Acceptance signal is concrete: the coin-network cards on https://dash.voidbind.com go from zero to non-zero.

@frstrtr

frstrtr commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

⚠️ CORRECTION — my "PR-specific" call was wrong. It is a systemic runner-network bug.

I said this Windows red was real and isolated to #891 because the same check passed on #930/#931/#880 and it survived a re-run. That reasoning was flawed.

ci-steward diagnosed the actual cause:

The curl 92 CANCELcurl 56 signature is an HTTP/2 stream reset on the runner→github.com path. Same bug killed #924's attribution-gate and #891 twice — but on two different physical hosts (c2pool-linux-198-1 vs VM217), which rules out a single-host cgroup wedge. Also independent of #931's -j4 cap.

So it is an intermittent network-egress failure that lands on different jobs on different PRs. It hit #891's Build secp256k1 step twice by chance, not by anything in the diff.

Where my inference broke: I treated "passes on three other PRs" as evidence of PR-specificity. For an intermittent fault that is exactly wrong — an intermittent failure will pass elsewhere by definition, and re-running only clears it when you win the coin-flip. Persistence across one re-run is not persistence.

That is now three CI mis-calls from me this week, and they do not share a direction:

The common thread is inferring cause from pass/fail distribution rather than reading the error. ci-steward read the curl exit codes. That is the difference.

The fix is already staged

git config --global http.version HTTP/1.1 plus postBuffer / lowSpeed tuning and a bounded fetch retry, applied runner-side so it covers every workflow without per-file edits. Tracked as #932 (git-fetch-retry composite action).

Action on this PR

Do not chase the diff. Nothing in #891 causes this. Once the runner-side git-config lands, re-run and it should go green.

The steward's own recommendation stands and I agree with it: land #891 clean as-is, and make the set_http_peer_seeds() wiring a fast-follow PR rather than bundling it here.

Apologies for the misdirection — if anyone spent time auditing the diff against a /bigobj or dependency theory, that was on my bad call.

@frstrtr
frstrtr merged commit 9b2865a into master Jul 28, 2026
79 of 86 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant